home *** CD-ROM | disk | FTP | other *** search
/ Clickx 47 / Clickx 47.iso / assets / software / Miro_Installer.exe / xulrunner / python / util.py < prev    next >
Encoding:
Python Source  |  2008-01-10  |  25.1 KB  |  751 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. import os
  19. import random
  20. import re
  21. import sys
  22. import sha
  23. import time
  24. import string
  25. import urllib
  26. import socket
  27. import logging
  28. import filetypes
  29. import tempfile
  30. import threading
  31. import traceback
  32. import subprocess
  33.  
  34. from clock import clock
  35. from types import UnicodeType, StringType
  36.  
  37. # Should we print out warning messages.  Turn off in the unit tests.
  38. chatter = True
  39.  
  40. inDownloader = False
  41. # this gets set to True when we're in the download process.
  42.  
  43. ignoreErrors = False
  44.  
  45. # Perform escapes needed for Javascript string contents.
  46. def quoteJS(x):
  47.     x = x.replace("\\", "\\\\") # \       -> \\
  48.     x = x.replace("\"", "\\\"") # "       -> \"  
  49.     x = x.replace("'",  "\\'")  # '       -> \'
  50.     x = x.replace("\n", "\\n")  # newline -> \n
  51.     x = x.replace("\r", "\\r")  # CR      -> \r
  52.     return x
  53.  
  54. def getNiceStack():
  55.     """Get a stack trace that's a easier to read that the full one.  """
  56.     stack = traceback.extract_stack()
  57.     # We don't care about the unit test lines
  58.     while (len(stack) > 0 and
  59.         os.path.basename(stack[0][0]) == 'unittest.py' or 
  60.         (isinstance(stack[0][3], str) and 
  61.             stack[0][3].startswith('unittest.main'))):
  62.         stack = stack[1:]
  63.     # remove after the call to util.failed
  64.     for i in xrange(len(stack)):
  65.         if (os.path.basename(stack[i][0]) == 'util.py' and 
  66.                 stack[i][2] in ('failed', 'failedExn')):
  67.             stack = stack[:i+1]
  68.             break
  69.     # remove trapCall calls
  70.     stack = [i for i in stack if i[2] != 'trapCall']
  71.     return stack
  72.  
  73. # Parse a configuration file in a very simple format. Each line is
  74. # either whitespace or "Key = Value". Whitespace is ignored at the
  75. # beginning of Value, but the remainder of the line is taken
  76. # literally, including any whitespace. There is no way to put a
  77. # newline in a value. Returns the result as a dict.
  78. def readSimpleConfigFile(path):
  79.     ret = {}
  80.  
  81.     f = open(path, "rt")
  82.     for line in f.readlines():
  83.         # Skip blank lines
  84.         if re.match("^[ \t]*$", line):
  85.             continue
  86.  
  87.         # Otherwise it'd better be a configuration setting
  88.         match = re.match(r"^([^ ]+) *= *([^\r\n]*)[\r\n]*$", line)
  89.         if not match:
  90.             print "WARNING: %s: ignored bad configuration directive '%s'" % (path, line)
  91.             continue
  92.         
  93.         key = match.group(1)
  94.         value = match.group(2)
  95.         if key in ret:
  96.             print "WARNING: %s: ignored duplicate directive '%s'" % (path, line)
  97.             continue
  98.  
  99.         ret[key] = value
  100.  
  101.     return ret
  102.  
  103. # Given a dict, write a configuration file in the format that
  104. # readSimpleConfigFile reads.
  105. def writeSimpleConfigFile(path, data):
  106.     f = open(path, "wt")
  107.  
  108.     for (k, v) in data.iteritems():
  109.         f.write("%s = %s\n" % (k, v))
  110.     
  111.     f.close()
  112.  
  113. # Called at build-time to ask Subversion for the revision number of
  114. # this checkout. Going to fail without Cygwin. Yeah, oh well. Pass the
  115. # file or directory you want to use as a reference point. Returns an
  116. # integer on success or None on failure.
  117. def queryRevision(f):
  118.     try:
  119.         p = subprocess.Popen(["svn", "info", f], stdout=subprocess.PIPE) 
  120.         info = p.stdout.read()
  121.         p.stdout.close()
  122.         url = re.search("URL: (.*)", info).group(1)
  123.         url = url.strip()
  124.         revision = re.search("Revision: (.*)", info).group(1)
  125.         revision = revision.strip()
  126.         return (url, revision)
  127.     except KeyboardInterrupt:
  128.         raise
  129.     except:
  130.         # whatever
  131.         return None
  132.  
  133. # 'path' is a path that could be passed to open() to open a file on
  134. # this platform. It must be an absolute path. Return the file:// URL
  135. # that would refer to the same file.
  136. def absolutePathToFileURL(path):
  137.     if isinstance(path, unicode):
  138.         path = path.encode("utf-8")
  139.     parts = string.split(path, os.sep)
  140.     parts = [urllib.quote(x, ':') for x in parts]
  141.     return "file://" + '/'.join(parts)
  142.  
  143.  
  144. # Shortcut for 'failed' with the exception flag.
  145. def failedExn(when, **kwargs):
  146.     failed(when, withExn = True, **kwargs)
  147.  
  148. # Puts up a dialog with debugging information encouraging the user to
  149. # file a ticket. (Also print a call trace to stderr or whatever, which
  150. # hopefully will end up on the console or in a log.) 'when' should be
  151. # something like "when trying to play a video." The user will see
  152. # it. If 'withExn' is true, last-exception information will be printed
  153. # to. If 'detail' is true, it will be included in the report and the
  154. # the console/log, but not presented in the dialog box flavor text.
  155. def failed(when, withExn = False, details = None):
  156.     logging.info ("failed() called; generating crash report.")
  157.  
  158.     header = ""
  159.     try:
  160.         import config # probably works at runtime only
  161.         import prefs
  162.         header += "App:        %s\n" % config.get(prefs.LONG_APP_NAME)
  163.         header += "Publisher:  %s\n" % config.get(prefs.PUBLISHER)
  164.         header += "Platform:   %s\n" % config.get(prefs.APP_PLATFORM)
  165.         header += "Python:     %s\n" % sys.version.replace("\r\n"," ").replace("\n"," ").replace("\r"," ")
  166.         header += "Py Path:    %s\n" % repr(sys.path)
  167.         header += "Version:    %s\n" % config.get(prefs.APP_VERSION)
  168.         header += "Serial:     %s\n" % config.get(prefs.APP_SERIAL)
  169.         header += "Revision:   %s\n" % config.get(prefs.APP_REVISION)
  170.         header += "Builder:    %s\n" % config.get(prefs.BUILD_MACHINE)
  171.         header += "Build Time: %s\n" % config.get(prefs.BUILD_TIME)
  172.     except KeyboardInterrupt:
  173.         raise
  174.     except:
  175.         pass
  176.     header += "Time:       %s\n" % time.asctime()
  177.     header += "When:       %s\n" % when
  178.     header += "\n"
  179.  
  180.     if withExn:
  181.         header += "Exception\n---------\n"
  182.         header += ''.join(traceback.format_exception(*sys.exc_info()))
  183.         header += "\n"
  184.     if details:
  185.         header += "Details: %s\n" % (details, )
  186.     header += "Call stack\n----------\n"
  187.     try:
  188.         stack = getNiceStack()
  189.     except KeyboardInterrupt:
  190.         raise
  191.     except:
  192.         stack = traceback.extract_stack()
  193.     header += ''.join(traceback.format_list(stack))
  194.     header += "\n"
  195.  
  196.     header += "Threads\n-------\n"
  197.     header += "Current: %s\n" % threading.currentThread().getName()
  198.     header += "Active:\n"
  199.     for t in threading.enumerate():
  200.         header += " - %s%s\n" % \
  201.             (t.getName(),
  202.              t.isDaemon() and ' [Daemon]' or '')
  203.  
  204.     # Combine the header with the logfile contents, if available, to
  205.     # make the dialog box crash message. {{{ and }}} are Trac
  206.     # Wiki-formatting markers that force a fixed-width font when the
  207.     # report is pasted into a ticket.
  208.     report = "{{{\n%s}}}\n" % header
  209.  
  210.     def readLog(logFile, logName="Log"):
  211.         try:
  212.             f = open(logFile, "rt")
  213.             logContents = "%s\n---\n" % logName
  214.             logContents += f.read()
  215.             f.close()
  216.         except KeyboardInterrupt:
  217.             raise
  218.         except:
  219.             logContents = ''
  220.         return logContents
  221.  
  222.     logFile = config.get(prefs.LOG_PATHNAME)
  223.     downloaderLogFile = config.get(prefs.DOWNLOADER_LOG_PATHNAME)
  224.     if logFile is None:
  225.         logContents = "No logfile available on this platform.\n"
  226.     else:
  227.         logContents = readLog(logFile)
  228.     if downloaderLogFile is not None:
  229.         if logContents is not None:
  230.             logContents += "\n" + readLog(downloaderLogFile, "Downloader Log")
  231.         else:
  232.             logContents = readLog(downloaderLogFile)
  233.  
  234.     if logContents is not None:
  235.         report += "{{{\n%s}}}\n" % stringify(logContents)
  236.  
  237.     # Dump the header for the report we just generated to the log, in
  238.     # case there are multiple failures or the user sends in the log
  239.     # instead of the report from the dialog box. (Note that we don't
  240.     # do this until we've already read the log into the dialog
  241.     # message.)
  242.     logging.info ("----- CRASH REPORT (DANGER CAN HAPPEN) -----")
  243.     logging.info (header)
  244.     logging.info ("----- END OF CRASH REPORT -----")
  245.  
  246.     if not inDownloader:
  247.         try:
  248.             import dialogs
  249.             from gtcache import gettext as _
  250.             if not ignoreErrors:
  251.                 chkboxdialog = dialogs.CheckboxTextboxDialog(_("Internal Error"),_("Miro has encountered an internal error. You can help us track down this problem and fix it by submitting an error report."), _("Include entire program database including all video and channel metadata with crash report"), False, _("Describe what you were doing that caused this error"), dialogs.BUTTON_SUBMIT_REPORT, dialogs.BUTTON_IGNORE)
  252.                 chkboxdialog.run(lambda x: _sendReport(report, x))
  253.         except Exception, e:
  254.             logging.exception ("Execption when reporting errror..")
  255.     else:
  256.         from dl_daemon import command, daemon
  257.         c = command.DownloaderErrorCommand(daemon.lastDaemon, report)
  258.         c.send()
  259.  
  260. def _sendReport(report, dialog):
  261.     def callback(result):
  262.         app.controller.sendingCrashReport -= 1
  263.         if result['status'] != 200 or result['body'] != 'OK':
  264.             logging.warning(u"Failed to submit crash report. Server returned %r" % result)
  265.         else:
  266.             logging.info(u"Crash report submitted successfully")
  267.     def errback(error):
  268.         app.controller.sendingCrashReport -= 1
  269.         logging.warning(u"Failed to submit crash report %r" % error)
  270.  
  271.     import dialogs
  272.     import httpclient
  273.     import config
  274.     import prefs
  275.     import app
  276.  
  277.     global ignoreErrors
  278.     if dialog.choice == dialogs.BUTTON_IGNORE:
  279.         ignoreErrors = True
  280.         return
  281.  
  282.     backupfile = None
  283.     if hasattr(dialog,"checkbox_value") and dialog.checkbox_value:
  284.         try:
  285.             logging.info("Sending entire database")
  286.             import database
  287.             backupfile = database.defaultDatabase.liveStorage.backupDatabase()
  288.         except:
  289.             traceback.print_exc()
  290.             logging.warning(u"Failed to backup database")
  291.  
  292.  
  293.     description = u"Description text not implemented"
  294.     if hasattr(dialog,"textbox_value"):
  295.         description = dialog.textbox_value
  296.  
  297.     description = description.encode("utf-8")
  298.     postVars = {"description":description,
  299.                 "app_name": config.get(prefs.LONG_APP_NAME),
  300.                 "log": report}
  301.     if backupfile:
  302.         postFiles = {"databasebackup": {"filename":"databasebackup.zip", "mimetype":"application/octet-stream", "handle":open(backupfile, "rb")}}
  303.     else:
  304.         postFiles = None
  305.     app.controller.sendingCrashReport += 1
  306.     httpclient.grabURL("http://participatoryculture.org/bogondeflector/index.php", callback, errback, method="POST", postVariables = postVars, postFiles = postFiles)
  307.  
  308. class AutoflushingStream:
  309.     """Converts a stream to an auto-flushing one.  It behaves in exactly the
  310.     same way, except all write() calls are automatically followed by a
  311.     flush().
  312.     """
  313.     def __init__(self, stream):
  314.         self.__dict__['stream'] = stream
  315.     def write(self, data):
  316.         if isinstance(data, unicode):
  317.             data = data.encode('ascii', 'backslashreplace')
  318.         self.stream.write(data)
  319.         self.stream.flush()
  320.     def __getattr__(self, name):
  321.         return getattr(self.stream, name)
  322.     def __setattr__(self, name, value):
  323.         return setattr(self.stream, name, value)
  324.  
  325. def makeDummySocketPair():
  326.     """Create a pair of sockets connected to each other on the local
  327.     interface.  Used to implement SocketHandler.wakeup().
  328.     """
  329.  
  330.     dummy_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  331.     dummy_server.bind( ('127.0.0.1', 0) )
  332.     dummy_server.listen(1)
  333.     server_address = dummy_server.getsockname()
  334.     first = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  335.     first.connect(server_address)
  336.     second, address = dummy_server.accept()
  337.     dummy_server.close()
  338.     return first, second
  339.  
  340. def trapCall(when, function, *args, **kwargs):
  341.     """Make a call to a function, but trap any exceptions and do a failedExn
  342.     call for them.  Return True if the function successfully completed, False
  343.     if it threw an exception
  344.     """
  345.  
  346.     try:
  347.         function(*args, **kwargs)
  348.         return True
  349.     except KeyboardInterrupt:
  350.         raise
  351.     except:
  352.         failedExn(when)
  353.         return False
  354.  
  355. # Turn the next flag on to track the cumulative time for each when argument to
  356. # timeTrapCall().  Don't do this for production builds though!  Since we never
  357. # clean up the entries in the cumulative dict, turning this on amounts to a
  358. # memory leak.
  359. TRACK_CUMULATIVE = False 
  360. cumulative = {}
  361. cancel = False
  362.  
  363. def timeTrapCall(when, function, *args, **kwargs):
  364.     global cancel
  365.     cancel = False
  366.     start = clock()
  367.     retval = trapCall (when, function, *args, **kwargs)
  368.     end = clock()
  369.     if cancel:
  370.         return retval
  371.     if end-start > 1.0:
  372.         logging.timing ("WARNING: %s too slow (%.3f secs)",
  373.             when, end-start)
  374.     if TRACK_CUMULATIVE:
  375.         try:
  376.             total = cumulative[when]
  377.         except KeyboardInterrupt:
  378.             raise
  379.         except:
  380.             total = 0
  381.         total += end - start
  382.         cumulative[when] = total
  383.         return retval
  384.         if total > 5.0:
  385.             logging.timing ("%s cumulative is too slow (%.3f secs)",
  386.                 when, total)
  387.             cumulative[when] = 0
  388.     cancel = True
  389.     return retval
  390.  
  391. def getTorrentInfoHash(path):
  392.     import libtorrent as lt
  393.     f = open(path, 'rb')
  394.     try:
  395.         data = f.read()
  396.         metainfo = lt.bdecode(data)
  397.         infohash = sha.sha(lt.bencode(metainfo['info'])).digest()
  398.         return infohash
  399.     finally:
  400.         f.close()
  401.  
  402. class ExponentialBackoffTracker:
  403.     """Utility class to track exponential backoffs."""
  404.     def __init__(self, baseDelay):
  405.         self.baseDelay = self.currentDelay = baseDelay
  406.     def nextDelay(self):
  407.         rv = self.currentDelay
  408.         self.currentDelay *= 2
  409.         return rv
  410.     def reset(self):
  411.         self.currentDelay = self.baseDelay
  412.  
  413.  
  414. # Gather movie files on the disk. Used by the startup dialog.
  415. def gatherVideos(path, progressCallback):
  416.     import item
  417.     import prefs
  418.     import config
  419.     import platformutils
  420.     keepGoing = True
  421.     parsed = 0
  422.     found = list()
  423.     try:
  424.         for root, dirs, files in os.walk(path):
  425.             for f in files:
  426.                 parsed = parsed + 1
  427.                 if filetypes.isVideoFilename(f):
  428.                     found.append(os.path.join(root, f))
  429.                 if parsed > 1000:
  430.                     adjustedParsed = int(parsed / 100.0) * 100
  431.                 elif parsed > 100:
  432.                     adjustedParsed = int(parsed / 10.0) * 10
  433.                 else:
  434.                     adjustedParsed = parsed
  435.                 keepGoing = progressCallback(adjustedParsed, len(found))
  436.                 if not keepGoing:
  437.                     found = None
  438.                     raise
  439.             if config.get(prefs.SHORT_APP_NAME) in dirs:
  440.                 dirs.remove(config.get(prefs.SHORT_APP_NAME))
  441.     except KeyboardInterrupt:
  442.         raise
  443.     except:
  444.         pass
  445.     return found
  446.  
  447. def formatSizeForUser(bytes, zeroString="", withDecimals=True, kbOnly=False):
  448.     """Format an int containing the number of bytes into a string suitable for
  449.     printing out to the user.  zeroString is the string to use if bytes == 0.
  450.     """
  451.     from gtcache import gettext as _
  452.     if bytes > (1 << 30) and not kbOnly:
  453.         value = (bytes / (1024.0 * 1024.0 * 1024.0))
  454.         if withDecimals:
  455.             format = _("%1.1fGB")
  456.         else:
  457.             format = _("%dGB")
  458.     elif bytes > (1 << 20) and not kbOnly:
  459.         value = (bytes / (1024.0 * 1024.0))
  460.         if withDecimals:
  461.             format = _("%1.1fMB")
  462.         else:
  463.             format = _("%dMB")
  464.     elif bytes > (1 << 10):
  465.         value = (bytes / 1024.0)
  466.         if withDecimals:
  467.             format = _("%1.1fKB")
  468.         else:
  469.             format = _("%dKB")
  470.     elif bytes > 1:
  471.         value = bytes
  472.         if withDecimals:
  473.             format = _("%1.1fB")
  474.         else:
  475.             format = _("%dB")
  476.     else:
  477.         return zeroString
  478.  
  479.     return format % value
  480.  
  481. def formatTimeForUser(seconds, sign=1):
  482.     """Format a duration in seconds into a string suitable for display, using
  483.     the minimum amount of digits. Negative durations used for remaining times
  484.     display a '-' sign.
  485.     """
  486.     _, _, _, h, m, s, _, _, _ = time.gmtime(seconds)
  487.     if sign < 0:
  488.         sign = '-'
  489.     else:
  490.         sign = ''
  491.     if int(seconds) in range(0, 3600):
  492.         return "%s%d:%02u" % (sign, m, s)
  493.     else:
  494.         return "%s%d:%02u:%02u" % (sign, h, m, s)
  495.  
  496. def makeAnchor(label, href):
  497.     return '<a href="%s">%s</a>' % (href, label)
  498.  
  499. def makeEventURL(label, eventURL):
  500.     return '<a href="#" onclick="return eventURL(\'action:%s\');">%s</a>' % \
  501.             (eventURL, label)
  502.  
  503. def clampText(text, maxLength):
  504.     if len(text) > maxLength:
  505.         return text[:maxLength-3] + '...'
  506.     else:
  507.         return text
  508.  
  509. def print_mem_usage(message):
  510.     pass
  511. # Uncomment for memory usage printouts on linux.
  512. #    print message
  513. #    os.system ("ps huwwwp %d" % (os.getpid(),))
  514.  
  515. class TooManySingletonsError(Exception):
  516.     pass
  517.  
  518. def getSingletonDDBObject(view):
  519.     view.confirmDBThread()
  520.     viewLength = view.len()
  521.     if viewLength == 1:
  522.         view.resetCursor()
  523.         return view.next()
  524.     elif viewLength == 0:
  525.         raise LookupError("Can't find singleton in %s" % repr(view))
  526.     else:
  527.         msg = "%d objects in %s" % (viewLength, len(view))
  528.         raise TooManySingletonsError(msg)
  529.  
  530. class ThreadSafeCounter:
  531.     """Implements a counter that can be access by multiple threads."""
  532.     def __init__(self, initialValue=0):
  533.         self.value = initialValue
  534.         self.lock = threading.Lock()
  535.  
  536.     def inc(self):
  537.         self.lock.acquire()
  538.         try:
  539.             self.value += 1
  540.         finally:
  541.             self.lock.release()
  542.  
  543.     def dec(self):
  544.         self.lock.acquire()
  545.         try:
  546.             self.value -= 1
  547.         finally:
  548.             self.lock.release()
  549.  
  550.     def getvalue(self):
  551.         self.lock.acquire()
  552.         try:
  553.             return self.value
  554.         finally:
  555.             self.lock.release()
  556.  
  557. def setupLogging():
  558.     logging.addLevelName(25, "TIMING")
  559.     logging.timing = lambda msg, *args, **kargs: logging.log(25, msg, *args, **kargs)
  560.     logging.addLevelName(26, "JSALERT")
  561.     logging.jsalert = lambda msg, *args, **kargs: logging.log(26, msg, *args, **kargs)
  562.  
  563.  
  564. # Returned when input to a template function isn't unicode
  565. class DemocracyUnicodeError(StandardError):
  566.     pass
  567.  
  568. # Raise an exception if input isn't unicode
  569. def checkU(text):
  570.     if text is not None and type(text) != UnicodeType:
  571.         raise DemocracyUnicodeError, (u"text \"%s\" is not a unicode string" %
  572.                                      text)
  573.  
  574. # Decorator that raised an exception if the function doesn't return unicode
  575. def returnsUnicode(func):
  576.     def checkFunc(*args, **kwargs):
  577.         result = func(*args,**kwargs)
  578.         if result is not None:
  579.             checkU(result)
  580.         return result
  581.     return checkFunc
  582.  
  583. # Raise an exception if input isn't a binary string
  584. def checkB(text):
  585.     if text is not None and type(text) != StringType:
  586.         raise DemocracyUnicodeError, (u"text \"%s\" is not a binary string" %
  587.                                      text)
  588.  
  589. # Decorator that raised an exception if the function doesn't return unicode
  590. def returnsBinary(func):
  591.     def checkFunc(*args, **kwargs):
  592.         result = func(*args,**kwargs)
  593.         if result is not None:
  594.             checkB(result)
  595.         return result
  596.     return checkFunc
  597.  
  598. # Raise an exception if input isn't a URL type
  599. def checkURL(text):
  600.     if type(text) != UnicodeType:
  601.         raise DemocracyUnicodeError, (u"url \"%s\" is not unicode" %
  602.                                      text)
  603.     try:
  604.         text.encode('ascii')
  605.     except:
  606.         raise DemocracyUnicodeError, (u"url \"%s\" contains extended characters" %
  607.                                      text)
  608.  
  609. # Decorator that raised an exception if the function doesn't return a filename
  610. def returnsURL(func):
  611.     def checkFunc(*args, **kwargs):
  612.         result = func(*args,**kwargs)
  613.         if result is not None:
  614.             checkURL(result)
  615.         return result
  616.     return checkFunc
  617.  
  618. # Returns exception if input isn't a filename type
  619. def checkF(text):
  620.     from platformutils import FilenameType
  621.     if text is not None and type(text) != FilenameType:
  622.         raise DemocracyUnicodeError, (u"text \"%s\" is not a valid filename type" %
  623.                                      text)
  624.  
  625. # Decorator that raised an exception if the function doesn't return a filename
  626. def returnsFilename(func):
  627.     def checkFunc(*args, **kwargs):
  628.         result = func(*args,**kwargs)
  629.         if result is not None:
  630.             checkF(result)
  631.         return result
  632.     return checkFunc
  633.  
  634. def unicodify(d):
  635.     """Turns all strings in data structure to unicode.
  636.     """
  637.     if isinstance(d, dict):
  638.         for key in d.keys():
  639.             d[key] = unicodify(d[key])
  640.     elif isinstance(d, list):
  641.         for key in range(len(d)):
  642.             d[key] = unicodify(d[key])
  643.     elif type(d) == StringType:
  644.         d = d.decode('ascii','replace')
  645.     return d
  646.  
  647. def stringify(u, handleerror="xmlcharrefreplace"):
  648.     """Takes a possibly unicode string and converts it to a string string.
  649.     This is required for some logging especially where the things being
  650.     logged are filenames which can be Unicode in the Windows platform.
  651.  
  652.     Note that this is not the inverse of unicodify.
  653.  
  654.     You can pass in a handleerror argument which defaults to "xmlcharrefreplace".
  655.     This will increase the string size as it converts unicode characters that
  656.     don't have ascii equivalents into escape sequences.  If you don't want to
  657.     increase the string length, use "replace" which will use ? for unicode
  658.     characters that don't have ascii equivalents.
  659.     """
  660.     if isinstance(u, unicode):
  661.         return u.encode("ascii", handleerror)
  662.     if not isinstance(u, str):
  663.         return str(u)
  664.     return u
  665.  
  666. def quoteUnicodeURL(url):
  667.     """Quote international characters contained in a URL according to w3c, see:
  668.     <http://www.w3.org/International/O-URL-code.html>
  669.     """
  670.     checkU(url)
  671.     quotedChars = list()
  672.     for c in url.encode('utf8'):
  673.         if ord(c) > 127:
  674.             quotedChars.append(urllib.quote(c))
  675.         else:
  676.             quotedChars.append(c)
  677.     return u''.join(quotedChars)
  678.  
  679. def no_console_startupinfo():
  680.     """Returns the startupinfo argument for subprocess.Popen so that we don't
  681.     open a console window.  On platforms other than windows, this is just
  682.     None.  On windows, it's some win32 sillyness.
  683.     """
  684.     if subprocess.mswindows:
  685.         startupinfo = subprocess.STARTUPINFO()
  686.         startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  687.         return startupinfo
  688.     else:
  689.         return None
  690.  
  691. def call_command(*args, **kwargs):
  692.     """Call an external command.  If the command doesn't exit with status 0,
  693.     or if it outputs to stderr, an exception will be raised.  Returns stdout.
  694.     """
  695.     ignore_stderr = kwargs.pop('ignore_stderr', False)
  696.     if kwargs:
  697.         raise TypeError('extra keyword arguments: %s' % kwargs)
  698.  
  699.     pipe = subprocess.Popen(args, stdout=subprocess.PIPE,
  700.             stdin=subprocess.PIPE, stderr=subprocess.PIPE,
  701.             startupinfo=no_console_startupinfo())
  702.     stdout, stderr = pipe.communicate()
  703.     if pipe.returncode != 0:
  704.         raise OSError("call_command with %s has return code %s\nstdout:%s\nstderr:%s" % 
  705.                 (args, pipe.returncode, stdout, stderr))
  706.     elif stderr and not ignore_stderr:
  707.         raise OSError("call_command with %s outputed error text:\n%s" % 
  708.                 (args, stderr))
  709.     else:
  710.         return stdout
  711.  
  712. def getsize(path):
  713.     """Get the size of a path.  If it's a file, return the size of the file.
  714.     If it's a directory return the total size of all the files it contains.
  715.     """
  716.  
  717.     if os.path.isdir(path):
  718.         size = 0
  719.         for (dirpath, dirnames, filenames) in os.walk(path):
  720.             for name in filenames:
  721.                 size += os.path.getsize(os.path.join(dirpath, name))
  722.             size += os.path.getsize(dirpath)
  723.         return size
  724.     else:
  725.         return os.path.getsize(path)
  726.  
  727. def partition(list, size):
  728.     """Partiction list into smaller lists such that none is larger than
  729.     size elements.
  730.  
  731.     Returns a list of lists.  The lists appended together will be the original
  732.     list.
  733.     """
  734.     retval = []
  735.     for start in range(0, len(list), size):
  736.         retval.append(list[start:start+size])
  737.     return retval
  738.  
  739. def directoryWritable(directory):
  740.     """Check if we can write to a directory."""
  741.     try:
  742.         f = tempfile.TemporaryFile(dir=directory)
  743.     except OSError:
  744.         return False
  745.     else:
  746.         f.close()
  747.         return True
  748.  
  749. def random_string(length):
  750.     return ''.join(random.choice(string.ascii_letters) for i in xrange(length))
  751.